package org.paradigmshift.cumulusfracto.filesystem;

import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Date;
import org.alfresco.config.ConfigElement;
import org.alfresco.config.element.GenericConfigElement;
import org.alfresco.jlan.netbios.NetBIOSName;
import org.alfresco.jlan.netbios.NetBIOSNameList;
import org.alfresco.jlan.netbios.NetBIOSSession;
import org.alfresco.jlan.netbios.win32.Win32NetBIOS;
import org.alfresco.jlan.server.auth.CifsAuthenticator;
import org.alfresco.jlan.server.config.CoreServerConfigSection;
import org.alfresco.jlan.server.config.GlobalConfigSection;
import org.alfresco.jlan.server.config.InvalidConfigurationException;
import org.alfresco.jlan.server.config.ServerConfiguration;
import org.alfresco.jlan.server.core.DeviceContextException;
import org.alfresco.jlan.server.filesys.DiskDeviceContext;
import org.alfresco.jlan.server.filesys.DiskInterface;
import org.alfresco.jlan.server.filesys.DiskSharedDevice;
import org.alfresco.jlan.server.filesys.FileSystem;
import org.alfresco.jlan.server.filesys.FilesystemsConfigSection;
import org.alfresco.jlan.server.filesys.SrvDiskInfo;
import org.alfresco.jlan.server.filesys.VolumeInfo;
import org.alfresco.jlan.smb.server.CIFSConfigSection;
import org.alfresco.jlan.util.IPAddress;
import org.alfresco.jlan.util.Platform;
import org.alfresco.jlan.util.X64;
import org.apache.log4j.Logger;


public class CumulusFractoServerConfiguration extends ServerConfiguration {
	
	private static final Logger log = Logger.getLogger( CumulusFractoServerConfiguration.class );
	
	private static final int[] DEFAULT_MEMORY_POOL_BUF_SIZES = { 256, 4096, 16384, 66000 };
	private static final int[] DEFAULT_MEMORY_POOL_INIT_ALLOC = { 20, 20, 5, 5 };
	private static final int[] DEFAULT_MEMORY_POOL_MAX_ALLOC = { 100, 50, 50, 50 };
	
	private static final int DEFAULT_THREAD_POOL_INIT = 25;
	private static final int DEFAULT_THREAD_POOL_MAX = 50;
	
	private static final String TOKEN_LOCAL_NAME = "${localname}";
	private static final String BIND_TO_IGNORE = "0.0.0.0";
	
	// Non configurable CIFS settings
	private static final String BROADCAST_ADDRESS = "255.255.255.255";
	private static final String BIND_TO = "127.0.0.1";
	
	// CIFS settings - In most cases these should not change.  Some of these may be turned into constants or removed altogether after testing.	   
	private boolean nativeCodeEnabled = false;
	private String hostName = "testHost";
	private String domainName = "testDomain";
	private String serverComment = "Cumulus Fracto Drive";
	
	private boolean netBiosEnabled = true;
	private int sessionPort = 1139;
	private int namePort = 1137;
	private int datagramPort = 1138;
	
	private boolean tcpIpEnabled = true;
	private int tcpIpPort = 1445;
	private boolean ipv6Enabled = false;
	
	private boolean win32NetBiosEnabled = true;
	private String win32Name = null;
	private String nativeApi = "winsock";
	
	public CumulusFractoServerConfiguration() {
		
		super( "Cumulus Fracto Server" );
	}
	
	public void init() throws InvalidConfigurationException {
		
		// *** Core Server Config ***
		CoreServerConfigSection coreConfig = new CoreServerConfigSection( this );
		coreConfig.setMemoryPool( DEFAULT_MEMORY_POOL_BUF_SIZES, DEFAULT_MEMORY_POOL_INIT_ALLOC, DEFAULT_MEMORY_POOL_MAX_ALLOC);
		coreConfig.setThreadPool( DEFAULT_THREAD_POOL_INIT, DEFAULT_THREAD_POOL_MAX );
		new GlobalConfigSection( this );
		new FilesystemsConfigSection( this );
		
		// *** CIFS Config ***
		CIFSConfigSection cifsConfig = new CIFSConfigSection( this );
		
		// *** Native Code Calls on Windows ***
		cifsConfig.setNativeCodeDisabled( !nativeCodeEnabled );
		if ( !nativeCodeEnabled ) log.warn( "CIFS server native calls disabled. JNI code will not be used." );
		
		// *** Network Broadcast Address ***
		if ( !IPAddress.isNumericAddress( BROADCAST_ADDRESS ) )
			throw new InvalidConfigurationException( "Invalid broadcast mask. Must be in n.n.n.n format." );
		cifsConfig.setBroadcastMask( BROADCAST_ADDRESS );
		
		// *** Hostname ***
		if ( hostName == null || hostName.length() == 0 )
			throw new InvalidConfigurationException( "Host name not specified or invalid." );
		
		// Insert the local host name into the host name if specified
		int tokenPos = hostName.indexOf( TOKEN_LOCAL_NAME );
		if ( tokenPos > -1 ) {
			
			String localHostName = null;
			if ( this.getPlatformType() == Platform.Type.WINDOWS && nativeCodeEnabled ) {
				
				// Get the local name via JNI
				localHostName = Win32NetBIOS.GetLocalNetBIOSName();
			}
			else {
				
				// Get the DNS name of the local system
				try {
					
					localHostName = InetAddress.getLocalHost().getHostName();
				}
				catch ( UnknownHostException uhe ) {}
			}
			
			// Strip the domain name
			if ( localHostName != null) {
				
				int domainPos = localHostName.indexOf( "." );
				if ( domainPos != -1 ) localHostName = localHostName.substring( 0, domainPos );
			}
			
			StringBuilder hostNameSb = new StringBuilder();
			hostNameSb.append( hostName.substring( 0, tokenPos ) );
			if ( localHostName != null ) hostNameSb.append( localHostName );
			hostNameSb.append( hostName.substring( tokenPos + TOKEN_LOCAL_NAME.length() ) );
			
			hostName = hostNameSb.toString();
			
			if ( hostName.equals( localHostName ) && getPlatformType() == Platform.Type.WINDOWS )
				throw new InvalidConfigurationException( "CIFS server name must be unique." );
		}
		
		if ( hostName.length() > 15 ) {
			
			// Truncate the CIFS server name
			hostName = hostName.substring( 0, 15 );
			log.warn( "CIFS server name is longer than 15 characters, truncated to " + hostName );
		}
		
		String upperCasedHostName = hostName.toUpperCase();
		cifsConfig.setServerName( upperCasedHostName );
		setServerName( upperCasedHostName );
		
		// *** Domain/Workgroup Name ***
		if ( domainName == null || domainName.length() <= 0 ) {
			
			if ( getPlatformType() == Platform.Type.WINDOWS && nativeCodeEnabled ) {
				
				// Get the local domain/workgroup name via JNI
				domainName = Win32NetBIOS.GetLocalDomainName();
				if ( log.isDebugEnabled() ) log.debug( "Local domain name is " + domainName + " (via JNI)" );
			}
			else {
				
				try {
					
					// Try and find the browse master on the local network
					NetBIOSName nbName = NetBIOSSession.FindName( NetBIOSName.BrowseMasterName, NetBIOSName.BrowseMasterGroup, 5000 );
					if ( log.isDebugEnabled() ) log.debug( "Found browse master at " + nbName.getIPAddressString( 0 ) );
					
					// Get the NetBIOS name list from the browse master
					NetBIOSNameList nbNameList = NetBIOSSession.FindNamesForAddress( nbName.getIPAddressString( 0 ) );
					if ( nbNameList != null ) {
						
						nbName = nbNameList.findName( NetBIOSName.MasterBrowser, false );
						if ( nbName != null ) domainName = nbName.getName();
					}
				}
				catch ( IOException ioe ) {}
			}
			
			if ( domainName == null && ( getPlatformType() != Platform.Type.WINDOWS || !nativeCodeEnabled ) ) {
				
				// Use a default domain/workgroup name
				domainName = "WORKGROUP";
				log.error( "Failed to get local domain/workgroup name, using default of " + domainName );
				log.error( "(This may be due to firewall settings or incorrect <broadcast> setting)" );
			}
		}
		cifsConfig.setDomainName( domainName.toUpperCase() );
		
		// *** Server Comment ***
		if ( serverComment != null ) cifsConfig.setComment( serverComment );
		
		// *** Bind Address ***
		if ( BIND_TO != null && !BIND_TO.equals( BIND_TO_IGNORE ) ) {
				
			// Validate the bind address
			try {
				
				InetAddress bindAddress = InetAddress.getByName( BIND_TO );
				cifsConfig.setSMBBindAddress( bindAddress );
			}
			catch ( UnknownHostException uhe ) {
				
				throw new InvalidConfigurationException( "Invalid CIFS server bind address: " + BIND_TO, uhe );
			}
		}
		else {
			
			throw new InvalidConfigurationException( "Invalid CIFS server bind address." );
		}
		
		
		// *** Authenticator ***
		String authClass = "org.paradigmshift.cumulusfracto.filesystem.CumulusFractoCifsAuthenticator";
		ConfigElement authenticatorElement = new GenericConfigElement( "authenticator" );
		authenticatorElement.addAttribute( "type", "cumulusfracto" );
		authenticatorElement.addChild( new ConfigElement( "class", authClass ) );
		cifsConfig.setAuthenticator( authClass, authenticatorElement, CifsAuthenticator.USER_MODE, false );
		
		// *** Host Announcer ***
//				cifsConfig.setHostAnnounceInterval( hostAnnounceInterval );
//				
//				// Check if the domain name has been set, this is required if the host announcer is enabled
//				if (cifsConfig.getDomainName() != null) {
//					
//					cifsConfig.setHostAnnouncer( !hostAnnounceDisabled );
//				}
//				else {
//					
//					log.error( "Domain name must be specified if host announcement is enabled" )
//				}
		
		// *** NetBIOS SMB ***
		cifsConfig.setNetBIOSSMB( netBiosEnabled );
		if ( netBiosEnabled && isValidPlatform( this.getPlatformType() ) ) {
				
			// Check for a bind address
			if ( BIND_TO != null && BIND_TO.length() > 0 && !BIND_TO.equals( BIND_TO_IGNORE ) ) {
				
				try {
					
					// Set the bind address for the NetBIOS name server
					InetAddress bindAddress = InetAddress.getByName( BIND_TO );
					cifsConfig.setNetBIOSBindAddress( bindAddress );
				}
				catch ( UnknownHostException uhe ){
					
					throw new InvalidConfigurationException( "Invalid NetBIOS bind address.", uhe );
				}
			}
			else {
				
				throw new InvalidConfigurationException( "Invalid NetBIOS bind address." );
			}
			
			// Check if the session port has been specified
			if ( !this.isValidPortNumber( sessionPort ) ) throw new InvalidConfigurationException( "NetBIOS session port out of valid range." );
			cifsConfig.setSessionPort( sessionPort );
			
			// Check if the name port has been specified
			if ( !this.isValidPortNumber( namePort ) ) throw new InvalidConfigurationException( "NetBIOS name port out of valid range." );
			cifsConfig.setNameServerPort( namePort );
			
			// Check if the datagram port has been specified
			if ( !this.isValidPortNumber( datagramPort ) ) throw new InvalidConfigurationException( "NetBIOS datagram port out of valid range." );
			cifsConfig.setDatagramPort( datagramPort );
		}
		
		// *** TCP/IP SMB ***
		cifsConfig.setTcpipSMB( tcpIpEnabled );
		if ( tcpIpEnabled && this.isValidPlatform( this.getPlatformType() ) ) {
			
			// Check if the port has been specified
			if ( !this.isValidPortNumber( tcpIpPort ) ) throw new InvalidConfigurationException( "TCP/IP SMB port out of valid range." );
			cifsConfig.setTcpipSMBPort( tcpIpPort );
			
			// Check if IPv6 support should be enabled
			if ( ipv6Enabled ) {
				
				try {
					
					// Use the IPv6 bind all address
					cifsConfig.setSMBBindAddress( InetAddress.getByName( "::" ) );
					if ( log.isInfoEnabled() ) log.info( "Enabled CIFS IPv6 bind address for native SMB." );
				}
				catch ( UnknownHostException uhe ) {
					
					throw new InvalidConfigurationException( "Failed to enable IPv6 bind address.", uhe );
				}
			}
		}
		
		// *** Win32 NetBIOS ***
		if ( win32NetBiosEnabled ) {
			
			// Check if the Win32 NetBIOS server name has been specified
			if ( win32Name != null && win32Name.length() > 0 ) {
	
				// Set the Win32 NetBIOS file server name
				if ( win32Name.length() > 16 ) throw new InvalidConfigurationException( "Invalid Win32 NetBIOS name: " + win32Name + "." );
				cifsConfig.setWin32NetBIOSName( win32Name );
			}
			
			// Check if the native NetBIOS interface has been specified, either 'winsock' or 'netbios'
			if ( nativeApi != null && nativeApi.length() > 0 ) {
				
				// Set the NetBIOS API to use
				boolean useWinsock = nativeApi.equalsIgnoreCase( "winsock" );
				if ( nativeApi.equalsIgnoreCase( "netbios" ) ) throw new InvalidConfigurationException( "Invalid NetBIOS API type. Specify 'winsock' or 'netbios'." );
				cifsConfig.setWin32WinsockNetBIOS( useWinsock );
			}
			
			// Force the older NetBIOS API code to be used on 64Bit Windows
			if ( cifsConfig.useWinsockNetBIOS() && X64.isWindows64() ) {
				
				// Use the older NetBIOS API code
				if ( log.isDebugEnabled() ) log.debug( "Using older Netbios() API code." );
				cifsConfig.setWin32WinsockNetBIOS( false );
			}
			
			// Check if the current operating system is supported by the Win32 NetBIOS handler
			String osName = System.getProperty( "os.name" );
			boolean isNetBiosSupported = nativeCodeEnabled && osName.startsWith( "Windows" ) && !osName.endsWith( "95" ) && !osName.endsWith( "98" ) && !osName.endsWith( "ME" );
			boolean nativeCodeInitialized = isNetBiosSupported && (Win32NetBIOS.LanaEnumerate() != null);
			cifsConfig.setWin32NetBIOS( isNetBiosSupported );
			
			if ( !nativeCodeInitialized ) log.warn( "No NetBIOS LANAs available" );
		}
		else {
			
			// Disable Win32 NetBIOS
			cifsConfig.setWin32NetBIOS( false );
		}
		
		// Check if NetBIOS and/or TCP/IP SMB have been enabled
		if ( !cifsConfig.hasNetBIOSSMB() && !cifsConfig.hasTcpipSMB() && !cifsConfig.hasWin32NetBIOS() )
			throw new InvalidConfigurationException( "NetBIOS SMB, TCP/IP SMB, or Win32 NetBIOS must be enabled." );
	}
	
	/**
	 * Add a disk share for Google Docs
	 * 
	 * @param docsService DocsService
	 * @param name String
	 * @param comment String
	 * @exception InvalidConfigurationException
	 */
	protected final void addDisk( String name, String comment ) throws InvalidConfigurationException {
		
		// Get the share name, and comment
		if ( name == null || name.length() <= 0 )
			throw new InvalidConfigurationException( "Disk share name must be specified" );
		
		// Check if a share with this name already exists
		FilesystemsConfigSection filesystemsConfig = (FilesystemsConfigSection) getConfigSection( FilesystemsConfigSection.SectionName );
		if ( filesystemsConfig.getShares().findShare( name ) != null )
			throw new InvalidConfigurationException( "Share " + name + " already exists" );
		
		// Create a device context and add the new disk share
		try {
			
			// Create the driver
			DiskInterface diskDrive = new CumulusFractoDiskDrive();
			
			// Create a context for this share instance, save the configuration parameters as part of the context
			GenericConfigElement params = new GenericConfigElement( "driver" );
			DiskDeviceContext deviceContext = (DiskDeviceContext) diskDrive.createContext( name, params );
			deviceContext.enableChangeHandler( false );
			deviceContext.setFilesystemAttributes( FileSystem.CasePreservedNames + FileSystem.UnicodeOnDisk );
			
			// Create volume information using the share name
			VolumeInfo volInfo = new VolumeInfo( name, (int) System.currentTimeMillis(), new Date( System.currentTimeMillis() ) );
			deviceContext.setVolumeInformation( volInfo );
			
			// Default to a 80Gb sized disk with 90% free space
			// TODO: set parameters to stuff from google docs
			SrvDiskInfo diskInfo = null; //new SrvDiskInfo( 2560000, 64, 512, 2304000 );
			deviceContext.setDiskInformation( diskInfo );
			deviceContext.setShareName( name );
			
			// Create the disk shared device and add to the server's list of shares
			DiskSharedDevice diskDev = new DiskSharedDevice( name, diskDrive, deviceContext );
			diskDev.setComment( comment );
			deviceContext.startFilesystem( diskDev );
			filesystemsConfig.addShare( diskDev );
		}
		catch ( DeviceContextException ex ) {
			
			throw new InvalidConfigurationException( "Driver context error: " + ex.getMessage() );
		}
		catch ( Exception ex ) {
			
			throw new InvalidConfigurationException( "Disk share setup error, " + ex.getMessage() );
		}
	}
	
	/**
	 * Remove a disk share for Google Docs
	 * 
	 * @param name String
	 * @exception InvalidConfigurationException
	 */
	protected final void removeDisk( String name ) throws InvalidConfigurationException {
		
		// Get the share name and comment
		if ( name == null || name.length() == 0 ) {
			
			throw new IllegalArgumentException( "Disk share name must be specified" );
		}
		
		// Check if a share with this name exists
		FilesystemsConfigSection filesystemsConfig = (FilesystemsConfigSection) getConfigSection( FilesystemsConfigSection.SectionName );
		if ( filesystemsConfig.getShares().findShare( name ) != null ) {
			
			filesystemsConfig.getShares().deleteShare( name );
		}
	}
	
	private boolean isValidPortNumber( int port ) {
		
		return ( port > 0 && port < 65535 );
	}
	
	private boolean isValidPlatform( Platform.Type type ) {
		
		boolean isValid = false;
		switch ( type ) {
			
			case LINUX:
			case MACOSX:
			case SOLARIS:
				isValid = true;
				break;
		}
		return isValid;
	}
}
